랭킹 캐시 구조 재설계
초기 랭킹 시스템은 전국 랭킹과 매장별 랭킹을 하나의 캐시 구조에서 관리하고 있었습니다.
그러나 서비스 규모가 증가하면서 실제 변경 범위보다 넓은 영역이 함께 갱신되는 문제가 발생하였고, 캐시 초기화 및 갱신 비용 역시 점차 증가하게 되었습니다.
이에 따라 전국 랭킹 캐시와 매장별 랭킹 캐시를 분리하는 방향으로 구조를 재설계하였으며, 변경 영향 범위를 축소하여 초기화 및 갱신 성능을 개선할 수 있었습니다.
개요
초기 랭킹 시스템은 전국 랭킹과 매장별 랭킹을 하나의 캐시 구조에서 관리하도록 설계되어 있었습니다.
당시에는 구현이 단순하고 관리가 용이하다는 장점이 있었으나, 서비스 규모가 증가하면서 캐시 갱신 범위가 지나치게 넓어지는 문제가 발생하였습니다.
특히 특정 매장의 기록이 변경되더라도 전국 단위 캐시 갱신 과정에 영향을 줄 수 있었으며, 이로 인해 캐시 초기화 및 갱신 비용 역시 함께 증가하게 되었습니다.
[1]당시 서비스는 매장 약 30개 내외를 대상으로 운영되고 있었으며, 맵 종류 역시 시즌 맵을 포함하여 약 7~8개 수준이었습니다.
또한 각 랭킹은 맵별 최대 500위까지 제공하고 있었기 때문에, 서비스 규모가 증가할수록 캐시 처리 범위 역시 함께 증가하는 구조였습니다.
이에 따라 랭킹 데이터의 활용 범위를 다시 분석하였고, 전국 랭킹 캐시와 매장별 랭킹 캐시를 분리하는 방향으로 구조를 재설계하게 되었습니다.
본 문서에서는 초기 캐시 구조와 문제 발생 원인, 그리고 캐시 구조 재설계 과정을 정리합니다.
초기 캐시 구조
초기 랭킹 시스템은 조회 성능을 우선시하여 전국 랭킹과 매장별 랭킹을 모두 캐싱하는 구조로 설계하였습니다.
당시에는 사용자가 랭킹을 조회할 때마다 정렬을 수행하는 대신, 미리 계산된 결과를 제공하는 것이 더 효율적이라고 판단하였습니다.
[2]전국 랭킹 → 맵별 Top 500
매장 랭킹 → 매장별 맵 Top 500
각 캐시는 맵별 Top Rank 정보를 미리 계산하여 보관하였으며, 사용자는 요청 시 별도의 정렬 과정 없이 캐시된 결과를 바로 전달받을 수 있었습니다.
또한 전국 랭킹과 매장 랭킹 모두 동일한 형태로 관리되었기 때문에 구현 초기에는 구조가 직관적이며 운영 역시 비교적 단순하다는 장점이 있었습니다.
하지만 서비스 규모가 증가하면서 랭킹 데이터의 변경 빈도 역시 증가하였고, 특정 매장의 기록이 변경되더라도 관련 캐시를 다시 생성해야 하는 경우가 발생하였습니다.
결과적으로 조회 비용을 줄이기 위해 도입한 캐시가 오히려 잦은 재생성 비용을 유발하게 되었으며, 이 과정에서 "무엇을 캐싱해야 하는가"보다 "어떤 단위가 안정적인 Cache Key가 될 수 있는가"에 대한 고민이 필요하게 되었습니다.
그 외에도 발생한 문제 및 자세한 문제사항은 아래 문제사항에서 확인 가능합니다.
문제 사항
초기 구조에서 가장 큰 문제는 랭킹 결과(Top Rank)를 직접 캐싱하고 있었다는 점이었습니다.
당시에는 조회 성능을 높이기 위해 전국 랭킹과 매장 랭킹의 Top Rank 결과를 미리 계산하여 저장하였으나, 랭킹 데이터가 변경될 때마다 해당 결과를 다시 계산해야 하는 구조적 한계를 가지고 있었습니다.
특히 매장 수와 사용자 수가 증가하면서 랭킹 갱신 빈도 역시 높아졌고, 단일 기록의 변경만으로도 관련 캐시를 재생성해야 하는 경우가 발생하였습니다.
결과적으로 조회 비용을 줄이기 위해 도입한 캐시가 오히려 잦은 재계산과 재생성 비용을 유발하게 되었으며, 일부 랭킹 조회 및 갱신 작업의 응답 시간이 약 10초 내외까지 증가하는 현상이 발생하였습니다.
또 다른 문제는 Cache Key의 안정성이었습니다.
초기에는 맵 이름과 같은 문자열 정보를 기준으로 캐시를 생성하였으나, 표기 방식의 차이와 문자열 처리에 대한 부담이 존재하였습니다.
비록 정규화(Normalization)를 통해 일부 문제를 완화할 수 있었지만, 문자열 자체를 식별자로 사용하는 구조는 장기적인 운영 관점에서 안정적인 설계라고 보기 어려웠습니다.
캐시 구조 재 설계
최종적으로는:
- Cache Key의 기준을 문자열 기반 맵 이름에서 변경 가능성이 거의 없는 맵 ID 기반으로 전환
- Top Rank 결과를 직접 캐싱하는 방식에서 벗어나, 매장(Store)과 맵(Map) 단위의 전체 랭킹 데이터를 캐싱하고 필요한 범위만 조회 시 계산하는 구조로 변경
이를 통해 문자열 처리로 인한 식별 문제를 제거할 수 있었습니다. 또한 맵의 이름 변동 시에도 ID를 기반으로 생성하였기 때문에 운영의 편리성이 증진하였습니다.
마지막으로 점핑배틀의 경우 경험치나 레벨 기반의 시스템이 아닌 일회성 게임의 점수에 대한 결과였기 때문에 캐싱의 단위를 늘림으로써 오히려 갱신 비용의 절약이 가능했습니다. 초기 구조에서는 최종 결과(Top Rank)를 캐싱하고 있었기 때문에, 원본 데이터가 변경될 경우 결과 전체를 다시 생성해야 하는 경우가 많았습니다.
반면 재설계 이후에는 랭킹 데이터 자체를 캐싱하고 조회 시 필요한 범위만 계산하도록 변경하였기 때문에, 데이터 변경 시에도 재생성 범위를 최소화할 수 있었습니다.
최종 결과
결과적으로 캐시 재생성 비용을 감소시킬 수 있었으며, 일부 랭킹 조회 및 갱신 작업의 응답 시간을 약 10초 내외에서 약 3초 수준까지 단축할 수 있었습니다.
또한 캐시 갱신 범위를 특정 매장 단위로 제한할 수 있게 되면서, 서비스 규모가 증가하더라도 영향 범위를 보다 명확하게 제어할 수 있게 되었습니다.
이를 통해 단순한 성능 개선뿐만 아니라, 향후 랭킹 기능 확장 시에도 보다 안정적으로 대응할 수 있는 기반을 마련할 수 있었습니다.
각주
당시 서비스는 매장 약 30개 내외를 대상으로 운영되고 있었으며, 맵 종류 역시 시즌 맵을 포함하여 약 7~8개 수준이었습니다.
또한 각 랭킹은 맵별 최대 500위까지 제공하고 있었기 때문에, 서비스 규모가 증가할수록 캐시 처리 범위 역시 함께 증가하는 구조였습니다.
전국 랭킹 → 맵별 Top 500
매장 랭킹 → 매장별 맵 Top 500